建明得知黃sir將與警方臥底於大廈見面,通知韓琛。韓琛一面派手下到大廈,一面進行毒品交易。黃sir為掩護永仁離開,被韓琛手下丟下大廈,寧願殉職而不發一言。黃sir死後,建明聯手永仁於停車場擊斃韓琛,最終兩人於警察局見面。當建明正幫永仁處理臥底檔案時,永仁發現其親手所寫帶有「標」字的信封竟然在建明桌上,頓時醒悟原來建明就是韓琛派至警隊的臥底,立即悄然離開。
自從黃sir殉職之後,警隊高層了解在趕快找出韓琛臥底的同時,也需要保護好自己派出的臥底,於是決定全面重新檢查一遍資料庫的存取權限。
經過一番資安演練,IT部門也發現平行時空的建明所發現的漏洞,於是高層決定做出以下變更:
PoliceSpy
新增兩個access policy
:
DCP
)可以insert
、update
及delete
。SP
)可以select
。PoliceSpyFile
方便各部門協同操作,只有警司級別以上(SP
)可以執行全部操作。REPL
操作。關於臥底資料僅提供一個呼叫list_police_spy_names()
的endpoint,且只有當操作者驗證為警司級別以上(SP
)且密碼正確的情況下,才能取得警隊全部臥底的名字。global
current_user_id
current_user_id
是一個global scalar
,讓我們在全域中都可以存取這個值。
global current_user_id: uuid;
可以透過set
這個指令來給定其值,如:
set global current_user_id:=<uuid>"ccc7a858-bd17-11ee-b4be-9f69662124af";
或透過reset
將其回復為預設值,如:
reset global current_user_id;
由於我們沒有給定預設值,所以如果執行上述query時,會將global current_user_id
變為空的<uuid>{}
。
需要留意global
為關鍵字,所以存取global scalar
時,global
關鍵字不可省略。
PoliceSpy
PoliceSpy
新增兩個access policy
:
type PoliceSpy extending Character, IsSpy {
access policy authorized_allow_insert_update_delete
allow insert, update, delete
using (
with police_officer:= (select IsPolice
filter .id = global current_user_id),
select if exists police_officer then (
police_officer.police_rank ?? PoliceRank.PC >= PoliceRank.DCP
) else (
false
)
){errmessage := "PoliceRank required: PoliceRank.DCP"};
access policy authorized_allow_select
allow select
using (
with police_officer:= (select IsPolice
filter .id = global current_user_id),
select if exists police_officer then (
police_officer.police_rank ?? PoliceRank.PC >= PoliceRank.SP
) else (
false
)
){errmessage := "PoliceRank required: PoliceRank.SP"};
}
我們在Envelope
已經學習過access policy
,這裡比較不一樣的是using
內比較複雜,我們舉第一個access policy
為例來看:
在with
區塊內,確認global current_user_id
現在所指定的id
的確在IsPolice
中。
接著在select
中使用if cond then {} else {}
的語法來判斷需要執行的query。我們用的判斷式是exists police_officer
:
police_officer
存在的話,我們執行police_officer.police_rank ?? PoliceRank.PC >= PoliceRank.DCP
。這段query的意思是判斷所找到police_officer
的police_rank
是否高於PoliceRank.DCP
,如果是的話,回傳true
,否則回傳false
。其中??
是當police_officer.police_rank
為空set
時的預設值,我們預設其為官階最小的PoliceRank.PC
。police_officer
不存在的話,回傳false
。這麼一來,我們滿足了第一個需求。
PoliceSpyFile
PoliceSpyFile
extending
Archive
而來,有一個link
、一個property
及一個access policy
:
colleagues
是multi link
指向PoliceSpy
。classified_info
property
為一str
,代表所儲存的機密資訊。access policy
的寫法與PoliceSpy
類似,但這邊是警司級別以上(SP
)可以執行全部操作。type PoliceSpyFile extending Archive {
multi colleagues: PoliceSpy;
classified_info: str;
access policy authorized_allow_all
allow all
using (
with police_officer:= (select IsPolice
filter .id = global current_user_id),
select if exists police_officer then (
police_officer.police_rank ?? PoliceRank.PC >= PoliceRank.SP
) else (
false
)
){ errmessage := "PoliceRank required: PoliceRank.SP"};
}
PoliceSpyFile
的妙用警司級別以上(SP
)可以對PoliceSpyFile
進行全部操作,包括select
colleagues
,這有可能會選取到多個PoliceSpy
。但要對這些PoliceSpy
進行update
或delete
依然需要為副處長級別以上(DCP
)。
PoliceSpyFile
可以想成一個專案簡報,當您具備足夠權限的時候,可以對這個簡報做任何操作,包括引用專案檔案(但如果權限不足的話,將無法更新或刪除專案檔案)。
這麼一來,我們滿足了第二個需求。
list_police_spy_names()
list_police_spy_names()
大多數情況應該會被某種web framework寫出來的程式所呼叫(例如Python
的FastAPI
或Rust
的Axum
)。
假如您有一個/policespy-names
的endpoint,可以用GET
來取得所有PolicySpy
name
的JSON
格式,那麼處理這個endpoint的view function
很有可能可以借助我們所寫的list_police_spy_names()
。
list_police_spy_names()
接收一個為str
的code
參數,並返回JSON
格式:
with
區塊,透過validate_password()
做驗證。如果通過的話,則返回所有PoliceSpyFile
,否則返回空set
(即<PoliceSpyFile>{}
)。with
區塊,透過array_agg()
將police_spy_file.colleagues.name
轉為array
,並存為names
。<json>(names)
的casting
功能返回JSON
格式。function list_police_spy_names(code: str) -> json
using (
with police_spy_file:= PoliceSpyFile if validate_password(code)
else <PoliceSpyFile>{},
names:= array_agg(police_spy_file.colleagues.name),
select json_object_pack({("names", <json>(names))})
);
要完成validate_password()
還需要搭配使用ext::pgcrypto、 morse_code_of_undercover
及get_stored_encrypted_password()
,我們繼續看下去。
ext::pgcrypto
...
using extension pg_trgm;
module default {
...
}
另外,如果您的app有驗證需求的話,可以試試EdgeDB4.0推出的Auth extension。
alias
morse_code_of_undercover
劇中永仁臥底檔案的密碼就是臥底的摩斯密碼。
根據網路上的搜尋結果,摩斯密碼大多是使用-
,但劇中卻是使用_
。讓我們尊重原著,使用內建的str_replace()將臥底的摩斯密碼中的-
換成_
,並存成alias
方便使用。
alias morse_code_of_undercover:= str_replace("..- -. -.. . .-. -.-. --- ...- . .-.", "-", "_");
實務上,不應該將機密資訊存為alias
。我們這麼做只是方便稍後展示validate_password()
及list_police_spy_names()
的效果。
alias
的function
新增test_scene09_alias()
並更新test_alias()
。
function test_alias() -> bool
using (all({
test_scene01_alias(),
test_scene02_alias(),
test_scene03_alias(),
test_scene05_alias(),
test_scene09_alias(),
})
);
function test_scene09_alias() -> bool
using (all({
(exists morse_code_of_undercover),
})
);
get_stored_encrypted_password()
get_stored_encrypted_password()
模擬自資料庫中取出hash過的加密密碼(雖然在這邊它看起來只是每次被呼叫時,計算morse_code_of_undercover
的hash值)。
function get_stored_encrypted_password() -> str
using (
with code:= morse_code_of_undercover,
module ext::pgcrypto,
select crypt(code, gen_salt())
);
validate_password()
最後我們參考官方文件中的範例,使用ext::pgcrypto::crypt()
來計算所輸入的密碼(salt
為hash過的加密密碼),是否會等於hash過的加密密碼本身。如果是的話,代表我們輸入的是正確密碼,返回true
,否則則返回false
。
function validate_password(code: str) -> bool
using (
with hash:= get_stored_encrypted_password(),
module ext::pgcrypto,
select crypt(code, hash) = hash
);
至此,我們滿足了第三個需求。
did you create extension 'pgcrypto'? [y,n,l,c,b,s,q,?]
> y
did you create alias 'default::morse_code_of_undercover'? [y,n,l,c,b,s,q,?]
> y
did you create function 'default::get_stored_encrypted_password'? [y,n,l,c,b,s,q,?]
> y
did you create function 'default::test_scene09_alias'? [y,n,l,c,b,s,q,?]
> y
did you create function 'default::validate_password'? [y,n,l,c,b,s,q,?]
> y
did you create global 'default::current_user_id'? [y,n,l,c,b,s,q,?]
> y
did you create object type 'default::PoliceSpyFile'? [y,n,l,c,b,s,q,?]
> y
did you create function 'default::list_police_spy_names'? [y,n,l,c,b,s,q,?]
> y
did you alter function 'default::test_alias'? [y,n,l,c,b,s,q,?]
> y
did you alter object type 'default::PoliceSpy'? [y,n,l,c,b,s,q,?]
> y
由於我們添加了兩個access policy
到PoliceSpy
,從現在開始每次select
PoliceSpy
時,都要時刻注意global current_user_id
所屬的object
是否有足夠權限。
test_alias()
由於test_alias()
中的test_scene09_alias()
含有chen
(PoliceSpy
)的測試,為了能夠select
到chen
來進行測試,我們從Police
中隨意挑選一個PoliceRank
為SP
的object
,將此object
的id
指定給global current_user_id
(由於目前SP
等級的警察只有黃sir一個,所以這個query就是將global current_user_id
設為黃sir的id
)。測試完成後,再執行reset global current_user_id
回復為預設值。
# end migration needs to be applied before running this query
set global current_user_id:= (select Police filter .police_rank=PoliceRank.SP limit 1).id;
select test_alias();
reset global current_user_id;
validate_password()
如果輸入正確的密碼,validate_password()
會回傳true
,否則回傳false
。
select validate_password(morse_code_of_undercover); # {true}
select validate_password("27149"); # {false}
PoliceSpy
、PoliceSpyFile
及list_police_spy_names()
PoliceRank
為PoliceRank.SP
與test_alias
一樣,我們將global current_user_id
設為黃sir的id
。
set global current_user_id:= (select Police filter .police_rank=PoliceRank.SP limit 1).id;
接著進行PoliceSpy
各項操作測試:
insert
會得到AccessPolicyError
。edgedb error: AccessPolicyError: access policy violation on insert of default::PoliceSpy (PoliceRank required: PoliceRank.DCP)
select
可以正常執行。update
及delete
會得到空set
。select PoliceSpy;
update PoliceSpy
filter .name="陳永仁"
set {
nickname:= .nickname ++ "!",
}; # {}
delete PoliceSpy filter .name="陳永仁"; # {}
這樣的結果符合需求一的部份要求。
再來進行PoliceSpyFile
各項操作測試及list_police_spy_names()
功能測試:
PoliceSpyFile
的各項功能皆能成功操作。list_police_spy_names()
在密碼正確的情況下,回傳含有資訊的JSON
資料,否則回傳空的JSON
資料。這樣的結果符合需求二及需求三。
insert PoliceSpyFile {
colleagues:= chen,
classified_info:= "Handler: test_SP...",
};
select PoliceSpyFile;
# {Json("{\"names\": [\"陳永仁\"]}")}
select list_police_spy_names(morse_code_of_undercover);
# {Json("{\"names\": []}")}
select list_police_spy_names("abc");
update PoliceSpyFile filter .classified_info="Handler: test_SP..."
set {
classified_info:= .classified_info ++ "..."
};
delete PoliceSpyFile;
回復global current_user_id
為預設值。
reset global current_user_id;
PoliceRank
為PoliceRank.DCP
由於資料庫中還沒有PoliceRank
為DCP
的Police object
,所以我們先insert
一個,再將其id
指定給global current_user_id
。
insert Police {name:= "test_DCP", police_rank:=PoliceRank.DCP};
set global current_user_id:= (select Police filter .police_rank=PoliceRank.DCP limit 1).id;
接著進行PoliceSpy
各項操作測試,皆能成功操作。
這樣的結果加上PoliceRank
為PoliceRank.SP
的測試,符合需求一的全部要求。
Insert PoliceSpy {name:= "test_police_spy_by_DPC"};
select PoliceSpy;
update PoliceSpy filter .name="test_police_spy_by_DPC"
set {
nickname:= "test_police_spy_by_DPC",
};
delete PoliceSpy filter .nickname="test_police_spy_by_DPC";
再來進行PoliceSpyFile
各項操作測試及list_police_spy_names()
功能測試:
PoliceSpyFile
的各項功能皆能成功操作。list_police_spy_names()
在密碼正確的情況下,回傳含有資訊的JSON
資料,否則回傳空的JSON
資料。這樣的結果符合需求二及需求三。
insert PoliceSpyFile {
colleagues:= chen,
classified_info:= "Handler: test_DCP...",
};
select PoliceSpyFile;
# {Json("{\"names\": [\"陳永仁\"]}")}
select list_police_spy_names(morse_code_of_undercover);
# {Json("{\"names\": []}")}
select list_police_spy_names("abc");
update PoliceSpyFile filter .classified_info="Handler: test_DCP..."
set {
classified_info:= .classified_info ++ "..."
};
delete PoliceSpyFile;
insert
ChenLauContact
insert ChenLauContact {
how:= "電話",
detail:= "黃sir殉職後,建明以黃sir手機聯絡永仁",
`when`:= assert_single((select FuzzyTime filter .fuzzy_fmt="2002/11/23_HH24:MI:SS_ID")),
where:= police_station union (insert Location {name:= "電車站"}),
};
insert ChenLauContact {
how:= "面對面",
detail:= "建明擊斃韓琛後,終於在警局與永仁見面,並確認其臥底身份。",
`when`:= assert_single((select FuzzyTime filter .fuzzy_fmt="2002/11/23_HH24:MI:SS_ID")),
where:= police_station,
};
insert
此場景的Scene
insert Scene {
title:= "真相大白",
detail:= "建明得知黃sir將與警方臥底於大廈見面,通知韓琛。韓琛一面派" ++
"手下到大廈,一面進行毒品交易。黃sir為掩護永仁離開,被韓琛" ++
"手下丟下樓,寧願殉職而不發一言。黃sir死後,建明聯手永仁於" ++
"停車場擊斃韓琛,最終兩人於警察局見面。當建明正幫永仁處理臥" ++
"底檔案時,永仁發現其親手所寫帶有「標」字的信封竟然在建明桌上," ++
"醒悟原來建明就是韓琛派至警隊的臥底,立即悄然離開。",
who:= (select Gangster filter .nickname in {"迪路", "傻強"})
union {wong, chen, hon, lau},
`when`:= assert_single(
(select FuzzyTime filter .fuzzy_fmt="2002/11/23_HH24:MI:SS_ID")
),
where:= (select Location filter .name in {"天台", "電車站"}) union
police_station union
(select(insert Location {name:="停車場"})),
};
# --8
刪除PoliceRank
為DCP
的測試用Police object
,並回復global current_user_id
為預設值。
delete Police filter .name="test_DCP";
reset global current_user_id;
我們假設劇中只有一個天台Location object
。
建明要刪掉永仁臥底檔案時,鏡頭內所帶到的資訊及其臥底時間,多處都與三部曲劇情不相吻合。